/*
 * Copyright 2002-2012 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.expression.spel;

import java.lang.reflect.Method;
import java.util.List;

import org.junit.Test;

import org.springframework.core.MethodParameter;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.expression.AccessException;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.Expression;
import org.springframework.expression.MethodExecutor;
import org.springframework.expression.MethodResolver;
import org.springframework.expression.PropertyAccessor;
import org.springframework.expression.TypeConverter;
import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.ReflectionHelper;
import org.springframework.expression.spel.support.StandardEvaluationContext;

import static org.junit.Assert.*;

///CLOVER:OFF

/**
 * Spring Security scenarios from https://wiki.springsource.com/display/SECURITY/Spring+Security+Expression-based+Authorization
 *
 * @author Andy Clement
 */
public class ScenariosForSpringSecurity extends AbstractExpressionTests {

    @Test
    public void testScenario01_Roles() throws Exception {
        try {
            SpelExpressionParser parser = new SpelExpressionParser();
            StandardEvaluationContext ctx = new StandardEvaluationContext();
            Expression expr = parser.parseRaw("hasAnyRole('MANAGER','TELLER')");

            ctx.setRootObject(new Person("Ben"));
            Boolean value = expr.getValue(ctx, Boolean.class);
            assertFalse(value);

            ctx.setRootObject(new Manager("Luke"));
            value = expr.getValue(ctx, Boolean.class);
            assertTrue(value);

        } catch (EvaluationException ee) {
            ee.printStackTrace();
            fail("Unexpected SpelException: " + ee.getMessage());
        }
    }

    @Test
    public void testScenario02_ComparingNames() throws Exception {
        SpelExpressionParser parser = new SpelExpressionParser();
        StandardEvaluationContext ctx = new StandardEvaluationContext();

        ctx.addPropertyAccessor(new SecurityPrincipalAccessor());

        // Multiple options for supporting this expression: "p.name == principal.name"
        // (1) If the right person is the root context object then "name==principal.name" is good enough
        Expression expr = parser.parseRaw("name == principal.name");

        ctx.setRootObject(new Person("Andy"));
        Boolean value = expr.getValue(ctx, Boolean.class);
        assertTrue(value);

        ctx.setRootObject(new Person("Christian"));
        value = expr.getValue(ctx, Boolean.class);
        assertFalse(value);

        // (2) Or register an accessor that can understand 'p' and return the right person
        expr = parser.parseRaw("p.name == principal.name");

        PersonAccessor pAccessor = new PersonAccessor();
        ctx.addPropertyAccessor(pAccessor);
        ctx.setRootObject(null);

        pAccessor.setPerson(new Person("Andy"));
        value = expr.getValue(ctx, Boolean.class);
        assertTrue(value);

        pAccessor.setPerson(new Person("Christian"));
        value = expr.getValue(ctx, Boolean.class);
        assertFalse(value);
    }

    @Test
    public void testScenario03_Arithmetic() throws Exception {
        SpelExpressionParser parser = new SpelExpressionParser();
        StandardEvaluationContext ctx = new StandardEvaluationContext();

        // Might be better with a as a variable although it would work as a property too...
        // Variable references using a '#'
        Expression expr = parser.parseRaw("(hasRole('SUPERVISOR') or (#a <  1.042)) and hasIpAddress('10.10.0.0/16')");

        Boolean value = null;

        ctx.setVariable("a", 1.0d); // referenced as #a in the expression
        ctx.setRootObject(new Supervisor("Ben")); // so non-qualified references 'hasRole()' 'hasIpAddress()' are invoked against it
        value = expr.getValue(ctx, Boolean.class);
        assertTrue(value);

        ctx.setRootObject(new Manager("Luke"));
        ctx.setVariable("a", 1.043d);
        value = expr.getValue(ctx, Boolean.class);
        assertFalse(value);
    }

    // Here i'm going to change which hasRole() executes and make it one of my own Java methods
    @Test
    public void testScenario04_ControllingWhichMethodsRun() throws Exception {
        SpelExpressionParser parser = new SpelExpressionParser();
        StandardEvaluationContext ctx = new StandardEvaluationContext();

        ctx.setRootObject(new Supervisor("Ben")); // so non-qualified references 'hasRole()' 'hasIpAddress()' are invoked against it);

        ctx.addMethodResolver(new MyMethodResolver()); // NEEDS TO OVERRIDE THE REFLECTION ONE - SHOW REORDERING MECHANISM
        // Might be better with a as a variable although it would work as a property too...
        // Variable references using a '#'
//		SpelExpression expr = parser.parseExpression("(hasRole('SUPERVISOR') or (#a <  1.042)) and hasIpAddress('10.10.0.0/16')");
        Expression expr = parser.parseRaw("(hasRole(3) or (#a <  1.042)) and hasIpAddress('10.10.0.0/16')");

        Boolean value = null;

        ctx.setVariable("a", 1.0d); // referenced as #a in the expression
        value = expr.getValue(ctx, Boolean.class);
        assertTrue(value);

//			ctx.setRootObject(new Manager("Luke"));
//			ctx.setVariable("a",1.043d);
//			value = (Boolean)expr.getValue(ctx,Boolean.class);
//			assertFalse(value);
    }


    static class Person {

        private String n;

        Person(String n) {
            this.n = n;
        }

        public String[] getRoles() {
            return new String[]{"NONE"};
        }

        public boolean hasAnyRole(String... roles) {
            if (roles == null) return true;
            String[] myRoles = getRoles();
            for (int i = 0; i < myRoles.length; i++) {
                for (int j = 0; j < roles.length; j++) {
                    if (myRoles[i].equals(roles[j])) return true;
                }
            }
            return false;
        }

        public boolean hasRole(String role) {
            return hasAnyRole(role);
        }

        public boolean hasIpAddress(String ipaddr) {
            return true;
        }

        public String getName() {
            return n;
        }
    }


    static class Manager extends Person {

        Manager(String n) {
            super(n);
        }

        @Override
        public String[] getRoles() {
            return new String[]{"MANAGER"};
        }
    }


    static class Teller extends Person {

        Teller(String n) {
            super(n);
        }

        @Override
        public String[] getRoles() {
            return new String[]{"TELLER"};
        }
    }


    static class Supervisor extends Person {

        Supervisor(String n) {
            super(n);
        }

        @Override
        public String[] getRoles() {
            return new String[]{"SUPERVISOR"};
        }
    }


    static class SecurityPrincipalAccessor implements PropertyAccessor {

        static class Principal {
            public String name = "Andy";
        }

        @Override
        public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException {
            return name.equals("principal");
        }

        @Override
        public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException {
            return new TypedValue(new Principal());
        }

        @Override
        public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException {
            return false;
        }

        @Override
        public void write(EvaluationContext context, Object target, String name, Object newValue)
                throws AccessException {
        }

        @Override
        public Class<?>[] getSpecificTargetClasses() {
            return null;
        }


    }


    static class PersonAccessor implements PropertyAccessor {

        Person activePerson;

        void setPerson(Person p) {
            this.activePerson = p;
        }

        @Override
        public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException {
            return name.equals("p");
        }

        @Override
        public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException {
            return new TypedValue(activePerson);
        }

        @Override
        public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException {
            return false;
        }

        @Override
        public void write(EvaluationContext context, Object target, String name, Object newValue)
                throws AccessException {
        }

        @Override
        public Class<?>[] getSpecificTargetClasses() {
            return null;
        }

    }


    static class MyMethodResolver implements MethodResolver {

        static class HasRoleExecutor implements MethodExecutor {

            TypeConverter tc;

            public HasRoleExecutor(TypeConverter typeConverter) {
                this.tc = typeConverter;
            }

            @Override
            public TypedValue execute(EvaluationContext context, Object target, Object... arguments)
                    throws AccessException {
                try {
                    Method m = HasRoleExecutor.class.getMethod("hasRole", String[].class);
                    Object[] args = arguments;
                    if (args != null) {
                        ReflectionHelper.convertAllArguments(tc, args, m);
                    }
                    if (m.isVarArgs()) {
                        args = ReflectionHelper.setupArgumentsForVarargsInvocation(m.getParameterTypes(), args);
                    }
                    return new TypedValue(m.invoke(null, args), new TypeDescriptor(new MethodParameter(m, -1)));
                } catch (Exception ex) {
                    throw new AccessException("Problem invoking hasRole", ex);
                }
            }

            public static boolean hasRole(String... strings) {
                return true;
            }
        }

        @Override
        public MethodExecutor resolve(EvaluationContext context, Object targetObject, String name, List<TypeDescriptor> arguments)
                throws AccessException {
            if (name.equals("hasRole")) {
                return new HasRoleExecutor(context.getTypeConverter());
            }
            return null;
        }
    }

}
